package ch.zhaw.cctd.domain.handlers;

import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.ConcurrentModificationException;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ch.zhaw.cctd.domain.Movable;
import ch.zhaw.cctd.domain.Shootable;
import ch.zhaw.cctd.domain.Shooter;
import ch.zhaw.cctd.domain.match.MatchServer;
import ch.zhaw.cctd.domain.prototype.CreepPrototype;
import ch.zhaw.cctd.domain.prototype.WavePrototype;
import ch.zhaw.cctd.domain.wave.Wave;
import ch.zhaw.cctd.domain.wave.Wave.Creeps;

/**
 * Eine Thread Klasse. Sie überwacht das komplette Spiel und berechnet stetig den neuen zustand des Spiels.
 * 
 * @author Rolf Koch
 * 
 */
public class GameThread implements Runnable {

	/**
	 * Logger
	 */
	private static final Logger logger = LoggerFactory.getLogger(GameThread.class);

	/**
	 * Standard Thread Sleep Time
	 */
	private static final int THREAD_SLEEP_TIME = 80;

	// Prevent Logging from Spamming
	long lastLog;

	// The MatchServer to get all other Game Objects
	private MatchServer matchServer;

	// Thread Killflag
	boolean killflag;
	// Running State
	boolean running;
	


	// eine Map<Zeit, Wave> der XML Wave Konfiguration. Zu welchem Zeitpunkt in
	// Sekunden nach Spielstart wird welche Wave gestartet wird.
	private ArrayList<Wave> waveList;

	// Saves the time when the last wave was started
	private long lastWaveSchedule;

	// Saves the time when the last Creep was started
	private long lastCreepSchedule;

	// Saves the current WaveCount
	private int waveCount;

	public int getWaveCount() {
		return waveCount;
	}

	/**
	 * Default constructor
	 * 
	 * @param map
	 */
	public GameThread(MatchServer matchServer) {
		logger.trace("Created ShooterHandler");
		this.matchServer = matchServer;
		this.killflag = false;
		this.running = false;

	}

	/**
	 * Used to initialize the WavesHandler with the WavePrototypes
	 * 
	 * @param wavePrototypes
	 */
	public void init(List<WavePrototype> wavePrototypes) {

		this.waveList = new ArrayList<Wave>();
		for (WavePrototype wavePrototype : wavePrototypes) {
			Wave wave = new Wave(wavePrototype);
			this.waveList.add(wave);
		}
		
		lastWaveSchedule = Calendar.getInstance().getTimeInMillis();
		lastCreepSchedule = Calendar.getInstance().getTimeInMillis();
	}

	/**
	 * Thread Start Method
	 */
	@Override
	public void run() {
		this.running = true;
		logger.trace("GameThread start");
		// Run until killflag is set
		while (!killflag) {

			try {
				// Command all Shoooters
				shoot(matchServer.getShooterList(), matchServer.getShootableList());

				// Move all Objects in the List
				move(matchServer.getMovableList());

				//Organize all Waves
				waves();

				// Sleep for a given amount of time
				Thread.sleep(THREAD_SLEEP_TIME);
			} catch (InterruptedException e) {
				if (killflag) {
					logger.debug("Uncritical InterruptedException: Stopping GameThread");
				} else {
					logger.warn("Unexpected InterruptedException in GameThread", e);
				}
			} catch (ConcurrentModificationException e) {
				logger.trace("Data was modified while GameThread was running, Interuppted.");
			}
		}
		logger.debug("Stopping GameThread");
		running = false;

	}

	/**
	 * Parse all Shooters and look if there is a Target for them.
	 * 
	 * @param shooterList
	 * @param shootableList
	 */
	public void shoot(List<? extends Shooter> shooterList, List<? extends Shootable> shootableList) throws ConcurrentModificationException {
		if (logger.isTraceEnabled()) {
			long nowTime = Calendar.getInstance().getTimeInMillis();
			if (lastLog + 1000 <= nowTime) {
				lastLog = nowTime;
				logger.trace("Shooting from {} Shooters(s) to {} Shootable(s)", shooterList.size(), shootableList.size());
			}

		}
		// Checking all Shooters
		for (Shooter t : shooterList) {

			// Create new Targetlist for the Shooter
			Shootable target = null;

			// Only search if the Shooter is Idle
			if (!t.hasTarget()) {

				int currentDistance = 99999;
				// Search the ShootableList for new Target
				for (Shootable c : shootableList) {
					// Check it only if its alive
					if (c.isAlive()) {

						// Calculate the Distance
						int distance = (int) c.getCenter().distance(t.getCenter());

						// if the Range is bigger than the Distance it is in
						// reach
						if (t.getRange() > distance) {

							// If the target is null or the creep is the
							// nearest creep, set it as next target
							// candidate
							if (target == null || currentDistance > distance) {
								logger.trace("TARGET");
								target = c;
								currentDistance = distance;
							}
						}
					}
				}

				// Assign new Target if it is available and matches abilities of
				// tower creep flying => tower needs to be flying too
				if (target != null && matchAbilities(t, target)) {
					logger.trace("target != null and abilities match");

					// Set Target
					t.setTarget(target);
					// Shoot
					t.shoot();
				}
			} else {
				// Has already a target: Shooting
				t.shoot();
			}
		}
	}

	/**
	 * Check if Shooter abilities are matching with Shootable Abilities
	 * 
	 * @param shooter
	 * @param shootable
	 * @return boolean
	 */
	private boolean matchAbilities(Shooter shooter, Shootable shootable) {

		for (String s : shooter.getAbilityList()) {
			logger.trace("Tower Ability: {}", s);
			if (shootable.hasAbility(s)) {
				logger.trace("matchAbilities returns: true");
				return true;
			}
		}

		return false;
	}

	/**
	 * Move all Movers if they are Moving
	 * 
	 * @param moverLists
	 */
	public void move(List<? extends Movable> moverList) {
		if (logger.isTraceEnabled()) {
			long nowTime = Calendar.getInstance().getTimeInMillis();
			if (lastLog + 1000 <= nowTime) {
				lastLog = nowTime;
				logger.debug("Moving {} Item(s)", moverList.size());
			}
		}

		// tell the Map to reevaluate the Movable List. Perhaps someone just
		// died.
		matchServer.updateMovableLists();
		
		// Move all Movers
		for (Movable m : moverList) {
			if (m.isMoving()) {
				m.move();
			}
		}
	}

	/**
	 * Wave Handling, Controlls the Spawning of all Creeps
	 */
	public void waves() {
		// Initialise the lastWaveSchedule with the Time this instance is
		// started
		if(waveList == null) {
			logger.trace("No Waves Found");
			return;
			
		}
		try {
			
			if(waveCount >= waveList.size()) {
				logger.trace("Win Game");
				matchServer.win();
				return;
			}
			
			Wave currentWave = waveList.get(waveCount);
			long nextWaveSchedule = lastWaveSchedule + (currentWave.getSpawnTime() * 1000);
			long currentTime = Calendar.getInstance().getTimeInMillis();


			if (nextWaveSchedule < currentTime) {
				
				logger.trace("time for Wave {}", currentTime - nextWaveSchedule );
				Wave wave = waveList.get(waveCount);
				// Its Time for a Wave

				if (!currentWave.isFinished()) {
					// Wave isn't finished yet

					int CreepSpawnInterval = currentWave.getSpawnLength() * 1000 / currentWave.getTotalCreeps();
					long nextCreepSchedule = lastCreepSchedule + CreepSpawnInterval;
					if (nextCreepSchedule < currentTime) {
						logger.trace("Its time for a Creep {}", currentTime-nextCreepSchedule);
						// Its time for a Creep
						boolean spawned = false;
						int size = currentWave.getCreepSets().size();

						switch (currentWave.getSpawnMode()) {
						case ROW:

							for (Creeps c : currentWave.getCreepSets()) {
								if (c.getAmount() > 0) {
									spawn(c);
									spawned = true;
									break;
								}
							}

							break;
						case MIXED:

							int currid = currentWave.getCurrPos() + 1;
							currid = currid % size;
							int startid = currid;
							// Check if the Creep can be spawned
							if (currentWave.getCreepSets().get(currid).getAmount() == 0) {
								// No, Creep cannot be spawned
								// Search next possible Creep
								do {
									// Sicle through all Creep Types
									currid++;
									currid = currid % size;
								} while (currentWave.getCreepSets().get(currid % size).getAmount() == 0 && startid != currid);

								// Check if we looped over the whole thing.
								if (startid == currid) {
									// Wave is finished
									wave.setFinished(true);
								} else if (currentWave.getCreepSets().get(currid).getAmount() != 0) {
									// No Loop: But Spawn is possible
									spawn(currentWave.getCreepSets().get(currid));
									spawned = true;
									currentWave.setCurrPos(currid);
								}
							} else {
								// Spawn is possible
								spawn(currentWave.getCreepSets().get(currid));
								spawned = true;
								currentWave.setCurrPos(currid);
							}

							break;
						case RANDOM:
							int id, start;
							if(size==1) {
								id = 0;
								start = 0;
							} else {
								double random = Math.random();
								id = (int) Math.round(random * (double) size);
								start = id;
							}
							// Check if the Creep can be spawned
							if (currentWave.getCreepSets().get(id).getAmount() == 0) {

								// No, Creep cannot be spawned

								// Search next possible Creep
								do {
									// Sicle through all Creep Types
									id++;
									id = id % size;
								} while (currentWave.getCreepSets().get(id % size).getAmount() == 0 && start != id);

								// Check if we looped over the whole thing.
								if (start == id) {
									// Wave is finished
									wave.setFinished(true);
								} else if (currentWave.getCreepSets().get(id).getAmount() != 0) {
									// No Loop: But Spawn is possible
									spawn(currentWave.getCreepSets().get(id));
									spawned = true;
								}
							} else {
								// Spawn is possible
								spawn(currentWave.getCreepSets().get(id));
								spawned = true;
							}
							break;
						}

						// Check if we are finished
						if (!spawned) {
							logger.trace("Wave finished");
							wave.setFinished(true);
						} else {
							logger.trace("lastCreepSchedule Set");
							lastCreepSchedule = currentTime;
						}

					} else {
						logger.trace("Nothing to do... Waiting");

					}
				} else {
					// Move lastWaveSchedule to the current Spot. The Next Wave
					// will then Wait their Waitingtime until it starts spawning
					lastWaveSchedule = lastWaveSchedule + (waveList.get(waveCount).getSpawnTime() * 1000);

					// Increment the Wave Counter to be ready for the next wave
					waveCount++;
					logger.trace("Preparing for next Wave");
				}
			}

			
		} catch (RemoteException e) {
			logger.error("Server Exception, could not spawn Creeps", e);
		}
	}

	public void spawn(Creeps c) throws RemoteException {
		logger.trace("Spawn Creep");
		matchServer.spawnCreep(matchServer.getPrototype(CreepPrototype.class, c.getCreepid()));
		c.lowerAmount();
	}

	/**
	 * Set the Stopflag
	 */
	public void stop() {
		killflag = true;
	}
	
	/**
	 * Returns running state of the Thread
	 * @return boolean
	 */
	public boolean isRunning() {
		return running;
	}
	
	/**
	 * Returns the Running state of the Thread
	 * @return boolean
	 */
	public boolean isWorking() {
		return !killflag;
	}

}
